﻿//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Microsoft Public License.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace AstoriaOverAstoria
{
    using System;
    using System.Collections.Generic;
    using System.Data.Services;
    using System.Data.Services.Client;
    using System.Data.Services.Providers;
    using System.Diagnostics;
    using System.Linq;
    using System.Xml;

    /// <summary>The data context for the AstoriaOverAstoria service.
    /// Typically this is the type parameter for the DataService type and its instance is returned from the CreateDataSource method.</summary>
    /// <remarks>This actually contains the implementation of the AstoriaOverAstoria service provider and all the logic.</remarks>
    public class AstoriaOverAstoriaContext :
        IDataServiceMetadataProvider,
        IDataServiceQueryProvider
    {
        /// <summary>Name of the context.</summary>
        private string contextName;

        /// <summary>The base uri of the service this context describes.</summary>
        private Uri serviceBaseUri;

        /// <summary>The metadata - that is the mapping between the client and server services.</summary>
        private MetadataMapping metadataMapping;

        /// <summary>The Astoria Client context used to run queries against the server.</summary>
        private DataServiceContext clientContext;

        /// <summary>Creates a new context.</summary>
        /// <param name="contextName">The name of the context.</param>
        /// <param name="metadataMapping">The metadata mapping to use.</param>
        /// <param name="context">Existing instance of the client context to use to connect to the server.
        /// Usually the caller should create new instance each time this AoA context is created to avoid tracking too many entities.
        /// Also this class assumes there are no pending changes and that it owns the context (nobody will be calling anything on it while in use by this class).
        /// The main purpose of this parameter is to allow caller to provide for example custom authentication on the context.</param>
        public AstoriaOverAstoriaContext(string contextName, MetadataMapping metadataMapping, DataServiceContext context)
        {
            this.contextName = contextName;
            this.metadataMapping = metadataMapping;
            this.metadataMapping.InitializeMetadataMapping();
            this.serviceBaseUri = context.BaseUri;
            this.clientContext = context;
            this.clientContext.IgnoreResourceNotFoundException = true;
        }

        /// <summary>Creates a new context.</summary>
        /// <param name="contextName">The name of the context.</param>
        /// <param name="metadataMapping">The metadata mapping to use.</param>
        /// <param name="serviceBaseUri">The uri of the server's service to run against.</param>
        public AstoriaOverAstoriaContext(string contextName, MetadataMapping metadataMapping, Uri serviceBaseUri)
        {
            this.contextName = contextName;
            this.metadataMapping = metadataMapping;
            this.metadataMapping.InitializeMetadataMapping();
            this.serviceBaseUri = serviceBaseUri;
            this.clientContext = new DataServiceContext(this.serviceBaseUri);
            this.clientContext.IgnoreResourceNotFoundException = true;
        }

        /// <summary>The Astoria Client context used to run the queries against the server.</summary>
        public DataServiceContext ClientContext
        {
            get { return this.clientContext; }
        }

        /// <summary>The mapping between the client and server metadata.,</summary>
        public MetadataMapping ContextMapping
        {
            get { return this.metadataMapping; }
        }

        /// <summary>Namespace name for the EDM container.</summary>
        public string ContainerName
        {
            get { return this.contextName; }
        }

        /// <summary>Name of the EDM container</summary>
        public string ContainerNamespace
        {
            get { return this.metadataMapping.Namespace; }
        }

        /// <summary>Gets all available containers.</summary>
        public IEnumerable<ResourceSet> ResourceSets
        {
            get { return this.metadataMapping.ResourceSets.Cast<ResourceSet>(); }
        }

        /// <summary>Returns all the service operations in this data source</summary>
        public IEnumerable<ServiceOperation> ServiceOperations
        {
            get { return new ServiceOperation[0]; }
        }

        /// <summary>Returns all the types in this data source</summary>
        public IEnumerable<ResourceType> Types
        {
            get { return this.metadataMapping.ResourceTypes.Cast<ResourceType>(); }
        }

        /// <summary>The data source from which data is provided.</summary>
        public object CurrentDataSource
        {
            get
            {
                return this;
            }

            set
            {
                Debug.Assert(value == this, "It should not be possible to create a different data source than this one.");
            }
        }

        /// <summary>Gets a value indicating whether null propagation is required in expression trees.</summary>
        public bool IsNullPropagationRequired
        {
            get
            {
                // In fact we would need something like half true and half false.
                // The client LINQ requires null checks in certain places, but doesn't support them in other places.
                // It is easier to add the checks into the required places (few of these) then to remove them from everywhere
                //   Areas where null checks are not needed (sometimes invalid): Filters, Projections of non-entity types
                //   Areas where null checks are needed: Projections of entity types
                return false;
            }
        }

        /// <summary>
        /// The method must return a collection of all the types derived from <paramref name="resourceType"/>.
        /// The collection returned should NOT include the type passed in as a parameter.
        /// An implementer of the interface should return null if the type does not have any derived types (ie. null == no derived types).
        /// </summary>
        /// <param name="resourceType">Resource to get derived resource types from.</param>
        /// <returns>
        /// A collection of resource types (<see cref="ResourceType"/>) derived from the specified <paramref name="resourceType"/> 
        /// or null if there no types derived from the specified <paramref name="resourceType"/> exist.
        /// </returns>
        public IEnumerable<ResourceType> GetDerivedTypes(ResourceType resourceType)
        {
            foreach (var rt in this.metadataMapping.ResourceTypes.Where(rt => rt != resourceType && rt.BaseType == resourceType))
            {
                yield return rt;

                foreach (var rs in ((IDataServiceMetadataProvider)this).GetDerivedTypes(rt))
                {
                    yield return rs;
                }
            }
        }

        /// <summary>
        /// Gets the ResourceAssociationSet instance when given the source association end.
        /// </summary>
        /// <param name="resourceSet">Resource set of the source association end.</param>
        /// <param name="resourceType">Resource type of the source association end.</param>
        /// <param name="resourceProperty">Resource property of the source association end.</param>
        /// <returns>ResourceAssociationSet instance.</returns>
        public ResourceAssociationSet GetResourceAssociationSet(ResourceSet resourceSet, ResourceType resourceType, ResourceProperty resourceProperty)
        {
            return this.metadataMapping.GetResourceAssociationSet(resourceSet, resourceType, resourceProperty);
        }

        /// <summary>
        /// Returns true if <paramref name="resourceType"/> represents an Entity Type which has derived Entity Types, else false.
        /// </summary>
        /// <param name="resourceType">instance of the resource type in question.</param>
        /// <returns>True if <paramref name="resourceType"/> represents an Entity Type which has derived Entity Types, else false.</returns>
        public bool HasDerivedTypes(ResourceType resourceType)
        {
            return this.metadataMapping.ResourceTypes.Any(rt => rt != resourceType && rt.BaseType == resourceType);
        }

        /// <summary>Given the specified name, tries to find a resource set.</summary>
        /// <param name="name">Name of the resource set to resolve.</param>
        /// <param name="resourceSet">Returns the resolved resource set, null if no resource set for the given name was found.</param>
        /// <returns>True if resource set with the given name was found, false otherwise.</returns>
        public bool TryResolveResourceSet(string name, out ResourceSet resourceSet)
        {
            ResourceSetMapping resourceSetMapping;
            if (this.metadataMapping.TryGetResourceSet(name, out resourceSetMapping))
            {
                resourceSet = resourceSetMapping;
                return true;
            }
            else
            {
                resourceSet = null;
                return false;
            }
        }

        /// <summary>Given the specified name, tries to find a type.</summary>
        /// <param name="name">Name of the type to resolve.</param>
        /// <param name="resourceType">Returns the resolved resource type, null if no resource type for the given name was found.</param>
        /// <returns>True if we found the resource type for the given name, false otherwise.</returns>
        public bool TryResolveResourceType(string name, out ResourceType resourceType)
        {
            ResourceTypeMapping resourceTypeMapping;
            if (this.metadataMapping.TryGetResourceType(name, out resourceTypeMapping))
            {
                resourceType = resourceTypeMapping;
                return true;
            }
            else
            {
                resourceType = null;
                return false;
            }
        }

        /// <summary>Given the specified name, tries to find a service operation.</summary>
        /// <param name="name">Name of the service operation to resolve.</param>
        /// <param name="serviceOperation">Returns the resolved service operation, null if no service operation was found for the given name.</param>
        /// <returns>True if we found the service operation for the given name, false otherwise.</returns>
        public bool TryResolveServiceOperation(string name, out ServiceOperation serviceOperation)
        {
            serviceOperation = null;
            return false;
        }

        /// <summary>
        /// Get the value of the open property.
        /// </summary>
        /// <param name="target">instance of the type declaring the open property.</param>
        /// <param name="propertyName">name of the open property.</param>
        /// <returns>value for the open property.</returns>
        public object GetOpenPropertyValue(object target, string propertyName)
        {
            throw new NotSupportedException("Open types are not supported");
        }

        /// <summary>
        /// Get the name and values of all the properties defined in the given instance of an open type.
        /// </summary>
        /// <param name="target">instance of a open type.</param>
        /// <returns>collection of name and values of all the open properties.</returns>
        public IEnumerable<KeyValuePair<string, object>> GetOpenPropertyValues(object target)
        {
            throw new NotSupportedException("Open types are not supported");
        }

        /// <summary>
        /// Get the value of the strongly typed property.
        /// </summary>
        /// <param name="target">instance of the type declaring the property.</param>
        /// <param name="resourceProperty">resource property describing the property.</param>
        /// <returns>value for the property.</returns>
        public object GetPropertyValue(object target, ResourceProperty resourceProperty)
        {
            return target.GetType().GetProperty(resourceProperty.Name).GetValue(target, null);
        }

        /// <summary>
        /// Returns the IQueryable that represents the resource set.
        /// </summary>
        /// <param name="resourceSet">resource set representing the entity set.</param>
        /// <returns>
        /// An IQueryable that represents the set; null if there is 
        /// no set for the specified name.
        /// </returns>
        public IQueryable GetQueryRootForResourceSet(ResourceSet resourceSet)
        {
            // Create a query on the client context
            var method = typeof(DataServiceContext).GetMethod("CreateQuery", new Type[] { typeof(string) });
            method = method.MakeGenericMethod(new Type[] { resourceSet.ResourceType.InstanceType });
            IQueryable sourceQuery = (IQueryable)method.Invoke(this.clientContext, new object[] { resourceSet.Name });

            // Wrap it in our own query and query provider
            AstoriaOverAstoriaQueryProvider queryProvider = new AstoriaOverAstoriaQueryProvider(this, sourceQuery.Provider);

            // Annotate the root of the query with the base resource type of the resource set
            queryProvider.SetResourceTypeAnnotation(sourceQuery.Expression, (ResourceTypeMapping)resourceSet.ResourceType);
            return ((IQueryProvider)queryProvider).CreateQuery(sourceQuery.Expression);
        }

        /// <summary>Gets the <see cref="ResourceType"/> for the specified <paramref name="target"/>.</summary>
        /// <param name="target">Target instance to extract a <see cref="ResourceType"/> from.</param>
        /// <returns>The <see cref="ResourceType"/> that describes this <paramref name="target"/> in this provider.</returns>
        public ResourceType GetResourceType(object target)
        {
            ResourceTypeMapping resourceTypeMapping;
            if (!this.metadataMapping.TryGetResourceType(target.GetType(), out resourceTypeMapping))
            {
                return null;
            }

            return resourceTypeMapping;
        }

        /// <summary>
        /// Invoke the given service operation and returns the results.
        /// </summary>
        /// <param name="serviceOperation">service operation to invoke.</param>
        /// <param name="parameters">value of parameters to pass to the service operation.</param>
        /// <returns>returns the result of the service operation. If the service operation returns void, then this should return null.</returns>
        public object InvokeServiceOperation(ServiceOperation serviceOperation, object[] parameters)
        {
            throw new NotImplementedException();
        }
    }
}